package org.mtmi.io;

import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Stack;

import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;

public class XMLWriter
{
	private static final int S_IN_ATTRIBUTE = 0; //in attributes area
	private static final int S_IN_CDATA     = 1; //in CDATA area, no more attribute, but not yet CDATA printed
	private static final int S_HAVE_CDATA   = 2; //CDATA printed
	
	private static class Element 
	{
		int state; 
		String name;
		public Element( String n ) {
			name = n;
			state = S_IN_ATTRIBUTE;
		}
	}
	
	private Stack<Element> elements; //stack of opened elements
	private PrintStream ps;
	private boolean indent_attr; //true if attributes are indented, false for 'inline'
	
	public XMLWriter( OutputStream os ) throws UnsupportedEncodingException
	{
		ps = new PrintStream( os, false, "UTF-8" ); //$NON-NLS-1$
		elements = new Stack<Element>();
		indent_attr= true;
		
		ps.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$
	}
	
	/** check if all element has been closed */
	public void endXML()
	{
		if( elements.size()>0 ) {
			String msg = "There are "+elements.size()+" element unclosed:\n";  //$NON-NLS-1$//$NON-NLS-2$
			for( Element E : elements ) {
				msg += "    "+E.name; //$NON-NLS-1$
			}
			throw new Error( msg );
		}
	}
	
	/**
	 * Print start element tag ("&lt;name" 
	 * printAttr() can be called to add attribute to element,
	 * use startCData() to close attributes and print CData,
	 * or use closeElement() to close element. 
	 * @param name
	 */
	public void startElement( String name ) {
		if( elements.size()>0 ) {
			Element elm = elements.lastElement();
			if( elm.state == S_IN_ATTRIBUTE ) {
				elm.state = S_IN_CDATA;
				ps.println(">"); //$NON-NLS-1$
			}
		}
		Element elm = new Element( name );
		elements.push( elm );
		indent(); ps.print("<"+name); //$NON-NLS-1$
	}
	
	public void printCData( String cdata ) {
		if( elements.size()>0 ) {
			Element elm = elements.lastElement();
			elm.state = S_HAVE_CDATA;
			ps.println(">"); //$NON-NLS-1$
		} else {
			throw new Error("cannot start CData section: no element opened"); //$NON-NLS-1$
		}
		ps.print(cdata);
	}
	
	public void endElement( String name ) {
		if( elements.size()>0 ) {
			Element elm = elements.lastElement();
			if( !name.equals( elm.name ) ) {
				throw new Error("cannot close element '"+name+"': current element is '"+elm.name+"'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}
			if( elm.state == S_IN_ATTRIBUTE ) {
				ps.println("/>"); //$NON-NLS-1$
			} else {
				if( elm.state != S_HAVE_CDATA ) {
					indent();				
				}
				ps.println("</"+name+">");  //$NON-NLS-1$//$NON-NLS-2$
			}
			elements.pop();
		} else {
			throw new Error("cannot close element '"+name+"': no element opened"); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}
	
	private void indent() {
		if( elements.size()>1 ) {
			int nc = 2*(elements.size()-1);
			StringBuilder sb = new StringBuilder( nc );
			for( int i=0; i<nc; i++ ) sb.append(' ');
			ps.print( sb.toString() );
		}
	}
	
	/** 
	 * Set attribute indented or not
	 * @param b new indentation value
	 * @return the repvious value of indentation flag, can be used to restore indentation.
	 */
	public boolean setAttributeIndented( boolean b ) {
		boolean old = indent_attr;
		indent_attr = b;
		return old;
	}
	
	/** add element with one attribute, element is closed (no need to call endElement()), value must not be null */
	public<T> void addElement( String name, String attr, T value )
	{
		boolean ia = setAttributeIndented( false );
		startElement( name );
		addAttr( attr, value );
		endElement( name );
		setAttributeIndented( ia );
	}
	
	/** add element with two attribute, element is closed (no need to call endElement()), values must not be null  */
	public<T,U> void addElement( String name, String attr1, T value1, String attr2, U value2 )
	{
		boolean ia = setAttributeIndented( false );
		startElement( name );
		addAttr( attr1, value1 );
		addAttr( attr2, value2 );
		endElement( name );
		setAttributeIndented( ia );
	}
	
	/** add element with 3 attributes, element is closed (no need to call endElement())  */
	public<T,U,V> void addElement( String name, String attr1, T value1, String attr2, U value2, String attr3, V value3 )
	{
		boolean ia = setAttributeIndented( false );
		startElement( name );
		addAttr( attr1, value1 );
		addAttr( attr2, value2 );
		addAttr( attr3, value3 );
		endElement( name );
		setAttributeIndented( ia );
	}
	
	/** convert any bad char to XML like " */
	public String convertToXml( String s )
	{
		s = s.replaceAll( "&",  "&amp;"  );  //$NON-NLS-1$//$NON-NLS-2$
		s = s.replaceAll( "\"", "&quot;" );  //$NON-NLS-1$//$NON-NLS-2$
		s = s.replaceAll( "'",  "&apos;" );  //$NON-NLS-1$//$NON-NLS-2$
		s = s.replaceAll( "<",  "&lt;"   );  //$NON-NLS-1$//$NON-NLS-2$
//TODO: sure there are others one ...		
		return s;
	}
	
	private void checkAttr() {
		if( elements.size()>0 ) {
			Element elm = elements.lastElement();
			if( elm.state != S_IN_ATTRIBUTE ) {
				throw new Error("Cannot add attribute: element '"+elm.name+"' is in CData section"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		} else {
			throw new Error("Cannot add attribute: no element opened"); //$NON-NLS-1$
		}
	}

	/** print the XML attribute, T can be: a String, a RGB, a Color, a Point, a Rectangle, otherwise T.toString() */
	public <T> void addAttr( String atr, T value ) {
		checkAttr();
		if( indent_attr ) {
			ps.print("\n"); //$NON-NLS-1$
			indent();
			ps.print("    "); //$NON-NLS-1$
		} else {
			ps.print("  "); //$NON-NLS-1$
		}
		String val;
		if( value instanceof String ) {
			val = convertToXml( (String)value );
		} else if( value instanceof Color ) {
			Color c = (Color)value;
			val = c.getRed()+":"+c.getGreen()+":"+c.getBlue();  //$NON-NLS-1$//$NON-NLS-2$
		} else if( value instanceof RGB ) {
			RGB c = (RGB)value;
			val = c.red+":"+c.green+":"+c.blue;  //$NON-NLS-1$//$NON-NLS-2$
		} else if( value instanceof Point ) {
			Point p = (Point)value;
			val = p.x+":"+p.y;
		} else if( value instanceof Rectangle ) {
			Rectangle r = (Rectangle)value;
			val = r.x+":"+r.y+":"+r.width+":"+r.height;
		} else {
			val = value.toString();
		}
		ps.print( atr+"=\""+ val + "\"" ); //$NON-NLS-1$ //$NON-NLS-2$
	}
	
	/** 
	 * Print the XML attribute only if value is not equals to default value.
	 * @see addAttr() for supported type
	 */
	public <T> void addAttr( String atr, T value, T default_value ) {
		if( value==default_value || value.equals( default_value ) ) return ;
		addAttr( atr, value );
	}
	
}
